/*
 *  ircd-hybrid: an advanced Internet Relay Chat Daemon(ircd).
 *  listener.c: Listens on a port.
 *
 *  Copyright (C) 2002 by the past and present ircd coders, and others.
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
 *  USA
 *
 *  $Id: listener.c 369 2006-05-02 21:34:55Z jon $
 */

#include "stdinc.h"
#include "listener.h"
#include "client.h"
#include "fdlist.h"
#include "irc_string.h"
#include "sprintf_irc.h"
#include "ircd.h"
#include "ircd_defs.h"
#include "s_bsd.h"
#include "irc_getnameinfo.h"
#include "irc_getaddrinfo.h"
#include "numeric.h"
#include "s_conf.h"
#include "s_stats.h"
#include "send.h"
#include "memory.h"
#include "tools.h"
#ifdef HAVE_LIBCRYPTO
#include <openssl/bio.h>
#endif


static PF accept_connection;

static dlink_list ListenerPollList = { NULL, NULL, 0 };
static void close_listener(struct Listener *listener);

static struct Listener *
make_listener(int port, struct irc_ssaddr *addr)
{
	struct Listener *listener = MyMalloc(sizeof(struct Listener));
	assert(listener != 0);

	listener->name = me.name;
	listener->port = port;
	memcpy(&listener->addr, addr, sizeof(struct irc_ssaddr));

	return listener;
}

void
free_listener(struct Listener *listener)
{
	assert(listener != NULL);

	if(listener == NULL)
		return;

	dlinkDelete(&listener->listener_node, &ListenerPollList);
	MyFree(listener);
}

/*
 * get_listener_name - return displayable listener name and port
 * returns "host.foo.org:6667" for a given listener
 */
const char *
get_listener_name(const struct Listener *listener)
{
	static char buf[HOSTLEN + HOSTLEN + PORTNAMELEN + 4];

	assert(listener != NULL);

	if(listener == NULL)
		return (NULL);

	ircsprintf(buf, "%s[%s/%u]", me.name, listener->name, listener->port);
	return (buf);
}

/* show_ports()
 *
 * inputs       - pointer to client to show ports to
 * output       - none
 * side effects - send port listing to a client
 */
void
show_ports(struct Client *source_p)
{
	char buf[4];
	char *p = NULL;
	dlink_node *ptr;

	DLINK_FOREACH(ptr, ListenerPollList.head)
	{
		const struct Listener *listener = ptr->data;
		p = buf;

		if(listener->flags & LISTENER_HIDDEN)
		{
			if(!IsAdmin(source_p))
				continue;
			*p++ = 'H';
		}

		if(listener->flags & LISTENER_SSL)
			*p++ = 's';
		*p = '\0';
		sendto_one(source_p, form_str(RPL_STATSPLINE),
			   me.name, source_p->name, 'P', listener->port,
			   IsOperAdmin(source_p) ? listener->name : me.name,
			   listener->ref_count, buf, listener->active ? "active" : "disabled");
	}
}

/*
 * inetport - create a listener socket in the AF_INET or AF_INET6 domain,
 * bind it to the port given in 'port' and listen to it
 * returns true (1) if successful false (0) on error.
 *
 * If the operating system has a define for SOMAXCONN, use it, otherwise
 * use HYBRID_SOMAXCONN
 */
#ifdef SOMAXCONN
#undef HYBRID_SOMAXCONN
#define HYBRID_SOMAXCONN SOMAXCONN
#endif

static int
inetport(struct Listener *listener)
{
	struct irc_ssaddr lsin;
	socklen_t opt = 1;

	/*
	 * At first, open a new socket
	 */
	if(comm_open(&listener->fd, listener->addr.ss.ss_family, SOCK_STREAM, 0,
		     "Listener socket") == -1)
	{
		report_error(L_ALL, "opening listener socket %s:%s",
			     get_listener_name(listener), errno);
		return 0;
	}

	memset(&lsin, 0, sizeof(lsin));
	memcpy(&lsin, &listener->addr, sizeof(struct irc_ssaddr));

	irc_getnameinfo((struct sockaddr *) &lsin, lsin.ss_len, listener->vhost,
			HOSTLEN, NULL, 0, NI_NUMERICHOST);
	listener->name = listener->vhost;

	/*
	 * XXX - we don't want to do all this crap for a listener
	 * set_sock_opts(listener);
	 */
	if(setsockopt(listener->fd.fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)))
	{
#ifdef _WIN32
		errno = WSAGetLastError();
#endif
		report_error(L_ALL, "setting SO_REUSEADDR for listener %s:%s",
			     get_listener_name(listener), errno);
		fd_close(&listener->fd);
		return (0);
	}

	/*
	 * Bind a port to listen for new connections if port is non-null,
	 * else assume it is already open and try get something from it.
	 */
	lsin.ss_port = htons(listener->port);

	if(bind(listener->fd.fd, (struct sockaddr *) &lsin, lsin.ss_len))
	{
#ifdef _WIN32
		errno = WSAGetLastError();
#endif
		report_error(L_ALL, "binding listener socket %s:%s",
			     get_listener_name(listener), errno);
		fd_close(&listener->fd);
		return (0);
	}

	if(listen(listener->fd.fd, HYBRID_SOMAXCONN))
	{
#ifdef _WIN32
		errno = WSAGetLastError();
#endif
		report_error(L_ALL, "listen failed for %s:%s", get_listener_name(listener), errno);
		fd_close(&listener->fd);
		return (0);
	}

	/* Listen completion events are READ events .. */

	accept_connection(&listener->fd, listener);
	return 1;
}

static struct Listener *
find_listener(int port, struct irc_ssaddr *addr)
{
	dlink_node *ptr;
	struct Listener *listener = NULL;
	struct Listener *last_closed = NULL;

	DLINK_FOREACH(ptr, ListenerPollList.head)
	{
		listener = ptr->data;

		if((port == listener->port) &&
		   (!memcmp(addr, &listener->addr, sizeof(struct irc_ssaddr))))
		{
			/* Try to return an open listener, otherwise reuse a closed one */
			if(!listener->fd.flags.open)
				last_closed = listener;
			else
				return (listener);
		}
	}

	return (last_closed);
}

/*
 * add_listener- create a new listener
 * port - the port number to listen on
 * vhost_ip - if non-null must contain a valid IP address string in
 * the format "255.255.255.255"
 */
void
add_listener(int port, const char *vhost_ip, unsigned int flags)
{
	struct Listener *listener;
	struct irc_ssaddr vaddr;
	struct addrinfo hints, *res;
	char portname[PORTNAMELEN + 1];
#ifdef IPV6
	static short int pass = 0;	/* if ipv6 and no address specified we need to
					   have two listeners; one for each protocol. */
#endif

	/*
	 * if no or invalid port in conf line, don't bother
	 */
	if(!(port > 0 && port <= 0xFFFF))
		return;

	memset(&vaddr, 0, sizeof(vaddr));

	/* Set up the hints structure */
	memset(&hints, 0, sizeof(hints));
	hints.ai_family = AF_UNSPEC;
	hints.ai_socktype = SOCK_STREAM;
	/* Get us ready for a bind() and don't bother doing dns lookup */
	hints.ai_flags = AI_PASSIVE | AI_NUMERICHOST;

#ifdef IPV6
	if(ServerInfo.can_use_v6)
	{
		snprintf(portname, PORTNAMELEN, "%d", port);
		irc_getaddrinfo("::", portname, &hints, &res);
		vaddr.ss.ss_family = AF_INET6;
		assert(res != NULL);

		memcpy((struct sockaddr *) &vaddr, res->ai_addr, res->ai_addrlen);
		vaddr.ss_port = port;
		vaddr.ss_len = res->ai_addrlen;
		irc_freeaddrinfo(res);
	}
	else
#endif
	{
		struct sockaddr_in *v4 = (struct sockaddr_in *) &vaddr;
		v4->sin_addr.s_addr = INADDR_ANY;
		vaddr.ss.ss_family = AF_INET;
		vaddr.ss_len = sizeof(struct sockaddr_in);
		v4->sin_port = htons(port);
	}

	snprintf(portname, PORTNAMELEN, "%d", port);

	if(vhost_ip)
	{
		if(irc_getaddrinfo(vhost_ip, portname, &hints, &res))
			return;

		assert(res != NULL);

		memcpy((struct sockaddr *) &vaddr, res->ai_addr, res->ai_addrlen);
		vaddr.ss_port = port;
		vaddr.ss_len = res->ai_addrlen;
		irc_freeaddrinfo(res);
	}
#ifdef IPV6
	else if(pass == 0 && ServerInfo.can_use_v6)
	{
		/* add the ipv4 listener if we havent already */
		pass = 1;
		add_listener(port, "0.0.0.0", flags);
	}
	pass = 0;
#endif

	if((listener = find_listener(port, &vaddr)))
	{
		listener->flags = flags;
		if(listener->fd.flags.open)
			return;
	}
	else
	{
		listener = make_listener(port, &vaddr);
		dlinkAdd(listener, &listener->listener_node, &ListenerPollList);
		listener->flags = flags;
	}

	if(inetport(listener))
		listener->active = 1;
	else
		close_listener(listener);
}

/*
 * close_listener - close a single listener
 */
static void
close_listener(struct Listener *listener)
{
	assert(listener != NULL);

	if(listener == NULL)
		return;

	if(listener->fd.flags.open)
		fd_close(&listener->fd);

	listener->active = 0;

	if(listener->ref_count)
		return;

	free_listener(listener);
}

/*
 * close_listeners - close and free all listeners that are not being used
 */
void
close_listeners(void)
{
	dlink_node *ptr;
	dlink_node *next_ptr;

	/* close all 'extra' listening ports we have */
	DLINK_FOREACH_SAFE(ptr, next_ptr, ListenerPollList.head) close_listener(ptr->data);
}

#define TOOFAST_WARNING "ERROR :Trying to reconnect too fast.\r\n"
#define DLINE_WARNING "ERROR :You have been D-lined.\r\n"

static void
accept_connection(fde_t * pfd, void *data)
{
	static time_t last_oper_notice = 0;
	struct irc_ssaddr addr;
	int fd;
	int pe;
	struct Listener *listener = data;

	memset(&addr, 0, sizeof(addr));

	assert(listener != NULL);
	if(listener == NULL)
		return;

	/* There may be many reasons for error return, but
	 * in otherwise correctly working environment the
	 * probable cause is running out of file descriptors
	 * (EMFILE, ENFILE or others?). The man pages for
	 * accept don't seem to list these as possible,
	 * although it's obvious that it may happen here.
	 * Thus no specific errors are tested at this
	 * point, just assume that connections cannot
	 * be accepted until some old is closed first.
	 */
	while((fd = comm_accept(listener, &addr)) != -1)
	{
		/*
		 * check for connection limit
		 */
		if(number_fd > hard_fdlimit - 10)
		{
			++ServerStats->is_ref;

			/*
			 * slow down the whining to opers bit
			 */
			if((last_oper_notice + 20) <= CurrentTime)
			{
				sendto_realops_flags(UMODE_ALL, L_ALL,
						     "All connections in use. (%s)",
						     get_listener_name(listener));
				last_oper_notice = CurrentTime;
			}

			if(!(listener->flags & LISTENER_SSL))
				send(fd, "ERROR :All connections in use\r\n", 32, 0);
#ifdef _WIN32
			closesocket(fd);
#else
			close(fd);
#endif
			break;	/* jump out and re-register a new io request */
		}

		/*
		 * Do an initial check we aren't connecting too fast or with too many
		 * from this IP...
		 */
		if((pe = conf_connect_allowed(&addr, addr.ss.ss_family)) != 0)
		{
			++ServerStats->is_ref;
			if(!(listener->flags & LISTENER_SSL))
				switch (pe)
				{
				case BANNED_CLIENT:
					send(fd, DLINE_WARNING, sizeof(DLINE_WARNING) - 1, 0);
					break;
				case TOO_FAST:
					send(fd, TOOFAST_WARNING, sizeof(TOOFAST_WARNING) - 1, 0);
					break;
				}

#ifdef _WIN32
			closesocket(fd);
#else
			close(fd);
#endif
			continue;	/* drop the one and keep on clearing the queue */
		}

		++ServerStats->is_ac;
		add_connection(listener, &addr, fd);
	}

	/* Re-register a new IO request for the next accept .. */
	comm_setselect(&listener->fd, COMM_SELECT_READ, accept_connection, listener, 0);
}
